﻿namespace UDKDevKitVSXDebugEngine
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Runtime.InteropServices;
    using Microsoft.VisualStudio.Debugger.Interop;
    using UDKDevKitVSXCore.Classes;
    using UDKDevKitVSXDebugEngine.Classes;
    using UDKDevKitVSXDebugEngine.Definitions;
    using UDKDevKitVSXDebugEngine.Exceptions;
    using UDKDevKitVSXDebugEngine.Utils;

    // 'Engine' class is the primary entrypoint object for UDK Script Debug Engine. 
    //
    // It implements:
    //
    // IDebugEngine2: This interface represents a debug engine (DE). It is used to manage various aspects of a debugging session, 
    // from creating breakpoints to setting and clearing exceptions.
    //
    // IDebugEngineLaunch2: Used by a debug engine (DE) to launch and terminate programs.
    //
    // IDebugProgram3: This interface represents a program that is running in a process. Since this engine only debugs one process at a time and each 
    // process only contains one program, it is implemented on the engine.
    //
    // IDebugEngineProgram2: This interface provides simultanious debugging of multiple threads in a debuggee.

    [ComVisible(true)]
    [Guid("CCD13A92-1C7F-418E-8951-F419A81DECBA")]
    public class Engine : IDebugEngine2, IDebugEngineLaunch2, IDebugProgram3, IDebugEngineProgram2
    {

        /// <summary>
        /// This is the engine GUID of UDK Script Debug Engine. It needs to be changed here and in the registration
        /// when creating a new engine.
        /// </summary>
        public const string EngineGuidId = "{179E815F-3C5E-4650-8E28-FC3C4EF6F92C}";

        // A unique identifier for the program being debugged.
        private Guid programId;
        private Guid processId;

        private ExecSimulator execSimulator;
        private UDKConduitEventsListener udkConduitEventsListener;
        private VSCommandSender vsCommandSender;

        // debug classes
        private EngineCallback callback;
        private Thread thread;
        private BreakpointManager breakpointManager;

        private DebugContext debugContext;
        private bool seenFirstContinue;

        private bool continueOnGo;

        public Engine()
        {
            Trace.WriteLine("Engine: new class.");

            this.processId = Guid.NewGuid();

            this.thread = new Thread(this);

            this.breakpointManager = new BreakpointManager(this);

            this.debugContext = new DebugContext();

            //GoogleAnalitics.TrackEvent("Debugger", "CreateEngine", "Debugger Engine Created", 1);
        }

        internal EngineCallback Callback
        {
            get { return this.callback; }
        }

        internal Thread Thread
        {
            get { return this.thread; }
        }

        internal DebugContext DebugContext
        {
            get { return this.debugContext; }
        }

        internal ExecSimulator ExecSimulator
        {
            get { return this.execSimulator; }
        }

        internal VSCommandSender VSCommandSender
        {
            get { return this.vsCommandSender; }
        }

        internal string SourcePath { get; private set; }

        internal bool IsContinueOnGo
        {
            get { return this.continueOnGo; }
        }

        internal IList<BoundBreakpoint> FindAllBoundExceptions()
        {
            return this.breakpointManager.FindAllBoundExceptions();
        }

        #region IDebugEngine2 Members

        // Attach the debug engine to a program. 
        int IDebugEngine2.Attach(IDebugProgram2[] rgpPrograms, IDebugProgramNode2[] rgpProgramNodes, uint celtPrograms, IDebugEventCallback2 ad7Callback, enum_ATTACH_REASON dwReason)
        {
            Debug.Assert(programId == Guid.Empty);

            Trace.WriteLine("Engine: Attach");

            if (celtPrograms != 1)
            {
                Debug.Fail("UDKScriptDebugEngine only expects to see one program in a process");
                throw new ArgumentException();
            }

            try
            {
                Guid processId = Core.GetProcessId(rgpPrograms[0]);
                if (processId == Guid.Empty)
                {
                    return Constants.E_NOTIMPL; // sample engine only supports system processes
                }

                string engineName;
                Guid engineGuidId;
                Core.CheckOk(rgpPrograms[0].GetEngineInfo(out engineName, out engineGuidId));

                if (engineGuidId != new Guid(EngineGuidId))
                {
                    return Constants.S_OK;
                }

                Core.RequireOk(rgpPrograms[0].GetProgramId(out programId));

                AD7EngineCreateEvent.Send(this);
                AD7ProgramCreateEvent.Send(this);

                // Attach can either be called to attach to a new process, or to complete an attach
                // to a launched process

                return Constants.S_OK;
            }
            catch (ComponentException e)
            {
                return e.HResult;
            }
            catch (Exception e)
            {
                return Core.UnexpectedException(e);
            }
        }

        // Requests that all programs being debugged by this DE stop execution the next time one of their threads attempts to run.
        // This is normally called in response to the user clicking on the pause button in the debugger.
        // When the break is complete, an AsyncBreakComplete event will be sent back to the debugger.
        int IDebugEngine2.CauseBreak()
        {
            return ((IDebugProgram2)this).CauseBreak();
        }

        // Called by the SDM to indicate that a synchronous debug event, previously sent by the DE to the SDM,
        // was received and processed. The only event UDK Script Debug Engine sends in this fashion is Program Destroy.
        // It responds to that event by shutting down the engine.
        int IDebugEngine2.ContinueFromSynchronousEvent(IDebugEvent2 eventObject)
        {
            Trace.WriteLine("Engine: ContinueFromSynchronousEvent");

            return Constants.S_OK;
        }

        // Creates a pending breakpoint in the engine. A pending breakpoint is contains all the information needed to bind a breakpoint to 
        // a location in the debuggee.
        int IDebugEngine2.CreatePendingBreakpoint(IDebugBreakpointRequest2 pBPRequest, out IDebugPendingBreakpoint2 ppPendingBP)
        {
            Trace.WriteLine("Engine: CreatePendingBreakpoint");

            ppPendingBP = null;

            try
            {
                this.breakpointManager.CreatePendingBreakpoint(pBPRequest, out ppPendingBP);
            }
            catch (Exception e)
            {
                return Core.UnexpectedException(e);
            }

            return Constants.S_OK;
        }

        // Informs a DE that the program specified has been atypically terminated and that the DE should 
        // clean up all references to the program and send a program destroy event.
        int IDebugEngine2.DestroyProgram(IDebugProgram2 pProgram)
        {
            // Tell the SDM that the engine knows that the program is exiting, and that the
            // engine will send a program destroy. We do this because the Win32 debug api will always
            // tell us that the process exited, and otherwise we have a race condition.

            Trace.WriteLine("Engine: DestroyProgram");

            return (HResults.E_PROGRAM_DESTROY_PENDING);
        }

        // Gets the GUID of the DE.
        int IDebugEngine2.GetEngineId(out Guid guidEngine)
        {
            Trace.WriteLine("Engine: GetEngineId");

            guidEngine = new Guid(EngineGuidId);
            return Constants.S_OK;
        }

        // Removes the list of exceptions the IDE has set for a particular run-time architecture or language.
        // UDK Script Debug Engine does not support exceptions in the debuggee so this method is not actually implemented.
        int IDebugEngine2.RemoveAllSetExceptions(ref Guid guidType)
        {
            Trace.WriteLine("Engine: RemoveAllSetExceptions Guid");

            return Constants.S_OK;
        }

        // Removes the specified exception so it is no longer handled by the debug engine.
        // UDK Script Debug Engine does not support exceptions in the debuggee so this method is not actually implemented.       
        int IDebugEngine2.RemoveSetException(EXCEPTION_INFO[] pException)
        {
            // UDK Script Debug Engine will always stop on all exceptions.
            Trace.WriteLine("Engine: RemoveSetException EXCEPTION_INFO");

            return Constants.S_OK;
        }

        // Specifies how the DE should handle a given exception.
        // UDK Script Debug Engine does not support exceptions in the debuggee so this method is not actually implemented.
        int IDebugEngine2.SetException(EXCEPTION_INFO[] pException)
        {
            Trace.WriteLine("Engine: SetException EXCEPTION_INFO");

            return Constants.S_OK;
        }

        // Sets the locale of the DE.
        // This method is called by the session debug manager (SDM) to propagate the locale settings of the IDE so that
        // strings returned by the DE are properly localized. UDK Script Debug Engine is not localized so this is not implemented.
        int IDebugEngine2.SetLocale(ushort wLangID)
        {
            Trace.WriteLine("Engine: SetLocale");

            return Constants.S_OK;
        }

        // A metric is a registry value used to change a debug engine's behavior or to advertise supported functionality. 
        // This method can forward the call to the appropriate form of the Debugging SDK Helpers function, SetMetric.
        int IDebugEngine2.SetMetric(string pszMetric, object varValue)
        {
            // UDK Script Debug Engine does not need to understand any metric settings.
            Trace.WriteLine("Engine: SetMetric");

            return Constants.S_OK;
        }

        // Sets the registry root currently in use by the DE. Different installations of Visual Studio can change where their registry information is stored
        // This allows the debugger to tell the engine where that location is.
        int IDebugEngine2.SetRegistryRoot(string pszRegistryRoot)
        {
            // UDK Script Debug Engine does not read settings from the registry.

            Trace.WriteLine("Engine: SetRegistryRoot");

            return Constants.S_OK;
        }

        #endregion

        #region IDebugEngineLaunch2 Members

        // Determines if a process can be terminated.
        int IDebugEngineLaunch2.CanTerminateProcess(IDebugProcess2 process)
        {
            Trace.WriteLine("Engine: CanTerminateProcess");

            return Constants.S_OK;
        }

        // Launches a process by means of the debug engine.
        // Normally, Visual Studio launches a program using the IDebugPortEx2::LaunchSuspended method and then attaches the debugger 
        // to the suspended program. However, there are circumstances in which the debug engine may need to launch a program 
        // (for example, if the debug engine is part of an interpreter and the program being debugged is an interpreted language), 
        // in which case Visual Studio uses the IDebugEngineLaunch2::LaunchSuspended method
        // The IDebugEngineLaunch2::ResumeProcess method is called to start the process after the process has been successfully launched in a suspended state.
        int IDebugEngineLaunch2.LaunchSuspended(string pszServer, IDebugPort2 port, string exe, string args, string dir, string env, string options, enum_LAUNCH_FLAGS launchFlags, uint hStdInput, uint hStdOutput, uint hStdError, IDebugEventCallback2 ad7Callback, out IDebugProcess2 process)
        {
            Trace.WriteLine("Engine: LaunchSuspended");

            AD_PROCESS_ID adProcessId = new AD_PROCESS_ID();
            adProcessId.ProcessIdType = (uint)enum_AD_PROCESS_ID.AD_PROCESS_ID_GUID;
            adProcessId.guidProcessId = this.processId;

            Core.RequireOk(port.GetProcess(adProcessId, out process));

            this.SourcePath = dir;

            this.callback = new EngineCallback(this, ad7Callback);
            this.execSimulator = new ExecSimulator(this);
            this.udkConduitEventsListener = new UDKConduitEventsListener(this.execSimulator);
            this.vsCommandSender = new VSCommandSender();

            return Constants.S_OK;
        }

        // Resume a process launched by IDebugEngineLaunch2.LaunchSuspended
        int IDebugEngineLaunch2.ResumeProcess(IDebugProcess2 process)
        {
            Trace.WriteLine("Engine: ResumeProcess");

            Debug.Assert(this.programId == Guid.Empty);

            try
            {
                Guid processId = Core.GetProcessId(process);

                // Send a program node to the SDM. This will cause the SDM to turn around and call IDebugEngine2.Attach
                // which will complete the hookup with AD7
                IDebugPort2 port;
                Core.RequireOk(process.GetPort(out port));

                IDebugDefaultPort2 defaultPort = (IDebugDefaultPort2)port;

                IDebugPortNotify2 portNotify;
                Core.RequireOk(defaultPort.GetPortNotify(out portNotify));

                Core.RequireOk(portNotify.AddProgramNode(new ProgramNode(this.processId)));

                if (this.programId == Guid.Empty)
                {
                    Debug.Fail("Unexpected problem -- IDebugEngine2.Attach wasn't called");
                    return Constants.E_FAIL;
                }

                // Resume the threads in the debuggee process
                this.execSimulator.RaiseDebugEventsOnResumeProcess();

                return Constants.S_OK;
            }
            catch (ComponentException e)
            {
                return e.HResult;
            }
            catch (Exception e)
            {
                return Core.UnexpectedException(e);
            }
        }

        // This function is used to terminate a process that the UDKScriptDebugEngine launched
        // The debugger will call IDebugEngineLaunch2::CanTerminateProcess before calling this method.
        int IDebugEngineLaunch2.TerminateProcess(IDebugProcess2 process)
        {
            Trace.WriteLine("Engine: TerminateProcess");

            try
            {
                this.vsCommandSender.StopDebugging();
                this.udkConduitEventsListener.StopListening();
            }
            finally
            {
                this.execSimulator.RaiseDebugEventsOnTerminateProcess();
            }

            //GoogleAnalitics.TrackEvent("Debugger", "Terminate Process", "Debugging Process Terminated", 1);

            return Constants.S_OK;
        }

        #endregion

        #region IDebugProgram2 Members

        // Determines if a debug engine (DE) can detach from the program.
        public int CanDetach()
        {
            // UDK Script Debug Engine always supports detach
            Trace.WriteLine("Engine: CanDetach");

            return Constants.S_OK;
        }

        // The debugger calls CauseBreak when the user clicks on the pause button in VS. The debugger should respond by entering
        // breakmode. 
        public int CauseBreak()
        {
            Trace.WriteLine("Engine: CauseBreak");

            this.vsCommandSender.Break();

            return Constants.S_OK;
        }

        // Continue is called from the SDM when it wants execution to continue in the debugee
        // but have stepping state remain. An example is when a tracepoint is executed, 
        // and the debugger does not want to actually enter break mode.
        public int Continue(IDebugThread2 pThread)
        {
            Trace.WriteLine("Engine: Continue");

            if (seenFirstContinue)
            {
                this.continueOnGo = true;

                this.vsCommandSender.Continue();
            }

            seenFirstContinue = true;

            return Constants.S_OK;
        }

        // Detach is called when debugging is stopped and the process was attached to (as opposed to launched)
        // or when one of the Detach commands are executed in the UI.
        public int Detach()
        {
            Trace.WriteLine("Engine: Detach");

            this.breakpointManager.ClearBoundBreakpoints();

            return Constants.S_OK;
        }

        // Enumerates the code contexts for a given position in a source file.
        public int EnumCodeContexts(IDebugDocumentPosition2 pDocPos, out IEnumDebugCodeContexts2 ppEnum)
        {
            Trace.WriteLine("Engine: EnumCodeContexts");

            throw new Exception("The method or operation is not implemented.");
        }

        // EnumCodePaths is used for the step-into specific feature -- right click on the current statment and decide which
        // function to step into. This is not something that the UDKScriptDebugEngine supports.
        public int EnumCodePaths(string hint, IDebugCodeContext2 start, IDebugStackFrame2 frame, int fSource, out IEnumCodePaths2 pathEnum, out IDebugCodeContext2 safetyContext)
        {
            Trace.WriteLine("Engine: EnumCodePaths");

            pathEnum = null;
            safetyContext = null;

            return Constants.E_NOTIMPL;
        }

        // EnumModules is called by the debugger when it needs to enumerate the modules in the program.
        public int EnumModules(out IEnumDebugModules2 ppEnum)
        {
            Trace.WriteLine("Engine: EnumModules");

            ppEnum = null;

            return Constants.E_NOTIMPL;
        }

        // EnumThreads is called by the debugger when it needs to enumerate the threads in the program.
        public int EnumThreads(out IEnumDebugThreads2 ppEnum)
        {
            Trace.WriteLine("Engine: EnumThreads");

            ppEnum = null;

            return Constants.E_NOTIMPL;
        }

        // The properties returned by this method are specific to the program. If the program needs to return more than one property, 
        // then the IDebugProperty2 object returned by this method is a container of additional properties and calling the 
        // IDebugProperty2::EnumChildren method returns a list of all properties.
        // A program may expose any number and type of additional properties that can be described through the IDebugProperty2 interface. 
        // An IDE might display the additional program properties through a generic property browser user interface.
        // UDK Script Debug Engine does not support this
        public int GetDebugProperty(out IDebugProperty2 ppProperty)
        {
            Trace.WriteLine("Engine: GetDebugProperty");

            throw new Exception("The method or operation is not implemented.");
        }

        // The debugger calls this when it needs to obtain the IDebugDisassemblyStream2 for a particular code-context.
        // UDK Script Debug Engine does not support dissassembly so it returns E_NOTIMPL
        public int GetDisassemblyStream(enum_DISASSEMBLY_STREAM_SCOPE dwScope, IDebugCodeContext2 codeContext, out IDebugDisassemblyStream2 disassemblyStream)
        {
            Trace.WriteLine("Engine: GetDisassemblyStream");

            disassemblyStream = null;

            return Constants.E_NOTIMPL;
        }

        // This method gets the Edit and Continue (ENC) update for this program. A custom debug engine always returns E_NOTIMPL
        public int GetENCUpdate(out object update)
        {
            // UDK Script Debug Engine does not participate in managed edit & continue.
            Trace.WriteLine("Engine: GetENCUpdate");

            update = null;
            return Constants.S_OK;
        }

        // Gets the name and identifier of the debug engine (DE) running this program.
        public int GetEngineInfo(out string engineName, out Guid engineGuid)
        {
            Trace.WriteLine("Engine: GetEngineInfo");

            engineName = ResourceStrings.EngineName;
            engineGuid = new Guid(Engine.EngineGuidId);

            return Constants.S_OK;
        }

        // The memory bytes as represented by the IDebugMemoryBytes2 object is for the program's image in memory and not any memory 
        // that was allocated when the program was executed.
        public int GetMemoryBytes(out IDebugMemoryBytes2 ppMemoryBytes)
        {
            Trace.WriteLine("Engine: GetMemoryBytes");

            throw new Exception("The method or operation is not implemented.");
        }

        // Gets the name of the program.
        // The name returned by this method is always a friendly, user-displayable name that describes the program.
        public int GetName(out string programName)
        {
            Trace.WriteLine("Engine: GetName");

            // UDK Script Debug Engine uses default transport and doesn't need to customize the name of the program,
            // so return NULL.
            programName = ResourceStrings.EngineName;

            return Constants.S_OK;
        }

        // Gets a GUID for this program. A debug engine (DE) must return the program identifier originally passed to the IDebugProgramNodeAttach2::OnAttach
        // or IDebugEngine2::Attach methods. This allows identification of the program across debugger components.
        public int GetProgramId(out Guid guidProgramId)
        {
            Debug.Assert(programId != Guid.Empty);

            Trace.WriteLine("Engine: GetProgramId");

            guidProgramId = programId;

            return Constants.S_OK;
        }

        // This method is deprecated. Use the IDebugProcess3::Step method instead.
        public int Step(IDebugThread2 pThread, enum_STEPKIND sk, enum_STEPUNIT Step)
        {
            Trace.WriteLine("Engine: Step");

            this.continueOnGo = false;

            switch (sk)
            {
                case enum_STEPKIND.STEP_BACKWARDS:
                    this.vsCommandSender.StepBackwards();
                    break;
                case enum_STEPKIND.STEP_INTO:
                    this.vsCommandSender.StepInto();
                    break;
                case enum_STEPKIND.STEP_OUT:
                    this.vsCommandSender.StepOut();
                    break;
                case enum_STEPKIND.STEP_OVER:
                    this.vsCommandSender.StepOver();
                    break;
                default:
                    break;
            }

            return Constants.S_OK;
        }

        // Terminates the program.
        public int Terminate()
        {
            // Because UDK Script Debug Engine is a native debugger, it implements IDebugEngineLaunch2, and will terminate
            // the process in IDebugEngineLaunch2.TerminateProcess
            Trace.WriteLine("Engine: Terminate");

            return Constants.S_OK;
        }

        // Writes a dump to a file.
        public int WriteDump(enum_DUMPTYPE DUMPTYPE, string pszDumpUrl)
        {
            // The sample debugger does not support creating or reading mini-dumps.
            Trace.WriteLine("Engine: WriteDump");

            return Constants.E_NOTIMPL;
        }

        #endregion

        #region IDebugProgram3 Members

        // ExecuteOnThread is called when the SDM wants execution to continue and have 
        // stepping state cleared.
        public int ExecuteOnThread(IDebugThread2 pThread)
        {
            Trace.WriteLine("Engine: ExecuteOnThread");

            if (seenFirstContinue)
            {
                this.continueOnGo = true;
                this.vsCommandSender.Continue();
            }

            return Constants.S_OK;
        }

        #endregion

        #region IDebugEngineProgram2 Members

        // Stops all threads running in this program.
        // This method is called when this program is being debugged in a multi-program environment. When a stopping event from some other program 
        // is received, this method is called on this program. The implementation of this method should be asynchronous; 
        // that is, not all threads should be required to be stopped before this method returns. The implementation of this method may be 
        // as simple as calling the IDebugProgram2::CauseBreak method on this program.
        //
        // UDK Script Debug Engine only supports debugging native applications and therefore only has one program per-process
        int IDebugEngineProgram2.Stop()
        {
            Trace.WriteLine("Engine: Stop");

            throw new Exception("The method or operation is not implemented.");
        }

        // WatchForExpressionEvaluationOnThread is used to cooperate between two different engines debugging 
        // the same process. UDK Script Debug Engine doesn't cooperate with other engines, so it has nothing
        // to do here.
        int IDebugEngineProgram2.WatchForExpressionEvaluationOnThread(IDebugProgram2 pOriginatingProgram, uint dwTid, uint dwEvalFlags, IDebugEventCallback2 pExprCallback, int fWatch)
        {
            Trace.WriteLine("Engine: WatchForExpressionEvaluationOnThread");

            return Constants.S_OK;
        }

        // WatchForThreadStep is used to cooperate between two different engines debugging the same process.
        // UDK Script Debug Engine doesn't cooperate with other engines, so it has nothing to do here.
        int IDebugEngineProgram2.WatchForThreadStep(IDebugProgram2 pOriginatingProgram, uint dwTid, int fWatch, uint dwFrame)
        {
            Trace.WriteLine("Engine: WatchForThreadStep");

            return Constants.S_OK;
        }

        #endregion

        #region Deprecated interface methods
        // These methods are not called by the Visual Studio debugger, so they don't need to be implemented

        int IDebugEngine2.EnumPrograms(out IEnumDebugPrograms2 programs)
        {
            Debug.Fail("This function is not called by the debugger");

            programs = null;
            return Constants.E_NOTIMPL;
        }

        public int Attach(IDebugEventCallback2 pCallback)
        {
            Debug.Fail("This function is not called by the debugger");

            return Constants.E_NOTIMPL;
        }

        public int GetProcess(out IDebugProcess2 process)
        {
            Trace.WriteLine("Engine: GetProcess");

            Debug.Fail("This function is not called by the debugger");

            process = null;
            return Constants.E_NOTIMPL;
        }

        public int Execute()
        {
            Trace.WriteLine("Engine: Execute");

            Debug.Fail("This function is not called by the debugger.");

            return Constants.E_NOTIMPL;
        }

        #endregion
    }
}
